/***************************************************************
*  Copyright (c) 2007 by GroupMe! Team (www.groupme.net)
*  All rights reserved.
*
*  This file is part of the GroupMe! Project. Source code of 
*  this project is closed and redistribution of this code is
*  prohibited. 
*  
*  Contact: http://www.groupme.net
*
*  This copyright notice MUST APPEAR in all copies of the file!
***************************************************************/
package net.groupme.search.strategy.folkrank;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import net.groupme.model.ObjectFactory;
import net.groupme.model.Tag;
import net.groupme.search.strategy.model.TAS;
import net.groupme.search.strategy.model.WeightedTag;


import cern.colt.matrix.DoubleMatrix1D;
import cern.colt.matrix.DoubleMatrix2D;
import cern.colt.matrix.impl.DenseDoubleMatrix1D;
import cern.colt.matrix.linalg.Algebra;
import cern.colt.matrix.linalg.SeqBlas;
import cern.jet.math.Functions;

/**
 * created on 10 Dec 2007
 * 
 * This class provides the basic FolkRank operations and is a kind utility
 * class for the different FolkRank-Search-Strategies...
 * 
 * @author Fabian Abel, <a href="mailto:abel@l3s.de">abel@l3s.de</a>
 * @author last edited by: $Author: fabian $
 * 
 * @version $Revision: 1.5 $ $Date: 2010-02-03 10:55:39 $
 */
public class FolkRank {
	/** set this to true to do some bad debugging */
	private static final boolean debug = false;
	
	/**
	 * Computes the FolkRank Vektor.
	 * @param adjacencyMatrix the Adjacency Matrix to use
	 * @param preferenceVector the preference vector of the user
	 * @return the FolkRank vector for the user.
	 */
	public static final DoubleMatrix1D computeFolkrank(DoubleMatrix2D adjacencyMatrix, DoubleMatrix1D preferenceVector){
	//		select w-init:
			DoubleMatrix1D w = new DenseDoubleMatrix1D(adjacencyMatrix.rows());
			w.assign(new Double(1.0/w.size()));
					
			//2. run adapted PageRank with d = 1
			DoubleMatrix1D w0 = computeAdaptedPageRank(adjacencyMatrix, 1.0, w, new DenseDoubleMatrix1D(adjacencyMatrix.rows()), FOLKRANK_SETTINGS.EPSILON, FOLKRANK_SETTINGS.MAX_ITERATIONS);
			
			//3. run adapted PageRank with d < 0
			DoubleMatrix1D w1 = computeAdaptedPageRank(adjacencyMatrix, FOLKRANK_SETTINGS.D, w, preferenceVector, FOLKRANK_SETTINGS.EPSILON, FOLKRANK_SETTINGS.MAX_ITERATIONS);
			
			//4. result = w1 - w0
			 w1.assign(w0, Functions.minus);
			 
			 return w1;
		}

	/**
	 * Adapted PageRank function...
	 * @param adjacencyMatrix weighted adjacency matrix
	 * @param d influence of personalization vector d = 0..1 
	 * @param w the initial ranking vektor
	 * @param p the personalization vector
	 * @param epsilon algorithm will stop when sum(abs(w_i+1[j] - w_i[j])) is smaller than epsilon
	 * @param maxIterations maximum number of iterations
	 * @return the ranking vektor
	 */
	public static final DoubleMatrix1D computeAdaptedPageRank(DoubleMatrix2D adjacencyMatrix, double d, DoubleMatrix1D w,  DoubleMatrix1D p, double epsilon, int maxIterations){
		double residual = 1.0; int k = 0;
		DoubleMatrix1D previousW = new DenseDoubleMatrix1D(w.size());
		Algebra alg = new Algebra();
		
	    //(1-d) * p:
	    SeqBlas.seqBlas.dscal(1.0-d, p);
	    if(debug){
	    	System.out.println("- debug - ");
	    	System.out.println("Preference Vector: " + p);
	    	System.out.println("1-norm of p: " + alg.norm1(p));
	    	System.out.println("W-Init Vector: " + w);
	    	System.out.println("1-norm of w: " + alg.norm1(w));
	    }
	    Double wValue = 0.0, prevValue = 0.0, tmpResidual = 0.0; 
		while(residual >= epsilon && k < maxIterations){
		    SeqBlas.seqBlas.dcopy(w, previousW);
		    
		    //d * A * w
		    w = alg.mult(adjacencyMatrix, w);
		    SeqBlas.seqBlas.dscal(d, w);
		    
		    //w = d * A * w + (1-d) * p:
		    w.assign(p, Functions.plus);
		    
		    if(debug){
		    	System.out.println("W Vector: " + w);
		    	System.out.println("1-norm of w: " + alg.norm1(w));
		    	System.out.println("previousW Vector: " + previousW);
		    	System.out.println("1-norm of previousW: " + alg.norm1(previousW));
		    	System.out.println("Same size? " + (previousW.size() == w.size()));
		    }
		    
		    //determine residual:
			residual = 0.0;
		    for(int i=0; i<w.size(); i++){
		    	wValue = w.get(i);
		    	prevValue = previousW.get(i);
		    	if(!wValue.isNaN() && !prevValue.isNaN()){
		    		tmpResidual = wValue - prevValue;
		    		if(!tmpResidual.isNaN()){
		    			residual += Math.abs(tmpResidual);
		    		}
		    	}else if(debug){
		    		System.out.println("NaN Exception: w[i] = " + wValue + ", prevW[i] = " + prevValue);
		    	}
		    }
		    k++;
		}
		if(debug ){
			System.out.println("Number of Adapted PageRank iterations: " + k);
	    }
		return w;
	}

	/**
	 * Computes the the weight of a link between a user and a tag.
	 * 
	 * @param tagAssignments list of tag assignments
	 * @param userId the user id
	 * @param tagId the tag id
	 * @return the weight of a link between the user and the tag (the higher the better)
	 * @throws SQLException
	 */
	public static final double computeUserTagWeight(List<TAS> tagAssignments, int userId, int tagId) throws SQLException{
		double score = 0.0;
		for (TAS tas : tagAssignments) {
			if(tas.getUserId() == userId && tas.getTagId() == tagId){
				if(tas.isDirectTAS()){
					score++;
				}else{
					score += FOLKRANK_SETTINGS.DAMPEN_FACTOR_OF_INHERIT_GROUPTAGS * score;
				}
			}
		}
		return score;
	}
	
	/**
	 * Computes the the weight of a link between a resource and a tag.
	 * 
	 * @param tagAssignments list of tag assignments
	 * @param tagId the tag id
	 * @param resourceId the resource id
	 * @return the weight of a link between the resource and the tag (the higher the better)
	 * @throws SQLException
	 */
	public static double computeTagResourceWeight(List<TAS> tagAssignments, int tagId, int resourceId) throws SQLException{
		double score = 0.0;
		for (TAS tas : tagAssignments) {
			if(tas.getResourceId() == resourceId && tas.getTagId() == tagId){
				if(tas.isDirectTAS()){
					score++;
				}else{
					score += FOLKRANK_SETTINGS.DAMPEN_FACTOR_OF_INHERIT_GROUPTAGS * score;
				}
			}
		}
		return score;
	}
	
	/**
	 * Computes the the weight of a link between a resource and a user.
	 * 
	 * @param tagAssignments list of tag assignments
	 * @param userId the user id
	 * @param resourceId the resource id
	 * @return the weight of a link between the resource and the user (the higher the better)
	 * @throws SQLException
	 */
	public static double computeUserResourceWeight(List<TAS> tagAssignments, int userId, int resourceId) throws SQLException{
		double score = 0.0;
		for (TAS tas : tagAssignments) {
			if(tas.getUserId() == userId && tas.getResourceId() == resourceId){
				if(tas.isDirectTAS()){
					score++;
				}else{
					score += FOLKRANK_SETTINGS.DAMPEN_FACTOR_OF_INHERIT_GROUPTAGS * score;
				}
			}
		}
		return score;
	}
	
	public static double computeUserTagWeightTXGY(List<TAS> tagAssignments, int userId, String tagGroupTag) throws SQLException{
		double score = 0.0;
		int tagId = new Integer(tagGroupTag.substring(1, tagGroupTag.indexOf(FolkRankWithTxGyTags.ARTIFICIAL_GROUP_TAG_SUFFIX)));
		int groupId = TAS.ID_NOT_SET;
		if(!tagGroupTag.contains(FolkRankWithTxGyTags.GROUP_NULL_INDICATOR)){
			groupId = new Integer(tagGroupTag.substring(tagGroupTag.indexOf(FolkRankWithTxGyTags.ARTIFICIAL_GROUP_TAG_SUFFIX)+1));
		}
		for (TAS tas : tagAssignments) {
			if(tas.getUserId() == userId){
				score += getArtificialTagSimilarity(tas, tagId, groupId);
			}
		}
		return score;
	}
	public static double computeTagResourceWeightTXGY(List<TAS> tagAssignments, int resourceId, String tagGroupTag) throws SQLException{
		double score = 0.0;
		int tagId = new Integer(tagGroupTag.substring(1, tagGroupTag.indexOf(FolkRankWithTxGyTags.ARTIFICIAL_GROUP_TAG_SUFFIX)));
		int groupId = TAS.ID_NOT_SET;
		if(!tagGroupTag.contains(FolkRankWithTxGyTags.GROUP_NULL_INDICATOR)){
			groupId = new Integer(tagGroupTag.substring(tagGroupTag.indexOf(FolkRankWithTxGyTags.ARTIFICIAL_GROUP_TAG_SUFFIX)+1));
		}
		for (TAS tas : tagAssignments) {
			if(tas.getResourceId() == resourceId){
				score += getArtificialTagSimilarity(tas, tagId, groupId);
			}
		}
		return score;
	}
	public static final double getArtificialTagSimilarity(TAS tas, int tagId, int groupId){
		if(groupId == tas.getGroupId() && tagId == tas.getTagId()){
			if(tas.isDirectTAS()){
				return FOLKRANK_SETTINGS.SAME_TAG_SAME_GROUP_SCORE;
			}else{
				return FOLKRANK_SETTINGS.DAMPEN_FACTOR_OF_INHERIT_GROUPTAGS * FOLKRANK_SETTINGS.SAME_TAG_SAME_GROUP_SCORE;
			}
		}else if(groupId != tas.getGroupId() && tagId == tas.getTagId()){
			if(tas.isDirectTAS()){
				return FOLKRANK_SETTINGS.SAME_TAG_DIFFERENT_GROUP_SCORE;
			}else{
				return FOLKRANK_SETTINGS.DAMPEN_FACTOR_OF_INHERIT_GROUPTAGS * FOLKRANK_SETTINGS.SAME_TAG_DIFFERENT_GROUP_SCORE;
			}
		}else  if(groupId == tas.getGroupId() && tagId != tas.getTagId()){
			if(tas.isDirectTAS()){
				return FOLKRANK_SETTINGS.DIFFERENT_TAG_SAME_GROUP_SCORE;
			}else{
				return FOLKRANK_SETTINGS.DAMPEN_FACTOR_OF_INHERIT_GROUPTAGS * FOLKRANK_SETTINGS.DIFFERENT_TAG_SAME_GROUP_SCORE;
			}
		}
		return 0.0;
	}

	
	/**
	 * Sets the preference vector for certain tags (given as keywords)
	 * @param p the preference vector that will be modified.
	 */
	public static void setPreferenceVektorForTags(DoubleMatrix1D p, List<Tag> tags, Map<Integer, Integer> tagIdColumMapping) throws SQLException{
		String[] tagArray = new String[tags.size()];
		for (int i = 0; i < tagArray.length; i++) {
			tagArray[i] = tags.get(i).getKeyword();
		}
		setPreferenceVektorForTags(p, tagArray, tagIdColumMapping);
	}
	
	/**
	 * Sets the preference vector for certain tags (given as keywords)
	 * @param p the preference vector that will be modified.
	 */
	public static void setPreferenceVektorForTags(DoubleMatrix1D p, String[] tags, Map<Integer, Integer> tagIdColumMapping) throws SQLException{
		Statement sqlStatement = null;
		ResultSet result = null;
		sqlStatement = ObjectFactory.ds.getConnection().createStatement();
		double weight = 1.0 / new Integer(tags.length).doubleValue();
		for (int i = 0; i < tags.length; i++) {
			result = sqlStatement.executeQuery("SELECT id FROM Tag where keyword = \"" + tags[i] + "\"");
			try{
			if(result.next()){
				p.set(tagIdColumMapping.get(result.getInt(1)), weight);
			}
			}catch (Exception e) {
				e.printStackTrace();
				System.out.println("Tag: " + tags[i]);
			}
		}
		ObjectFactory.ds.getConnection().close();
	}
	
	/**
	 * Sets the preference vector for certain weighted tags (given as list of {@link WeightedTag}).
	 * 
	 * <b>Important:</b> the ID of the weighted tags has to be set!!!
	 * 
	 * @param p the preference vector that will be modified.
	 * @param query the actual query (id has to be set!!)
	 * @param influenceOfPersonalization the influence of the profile in comparison to the actual query, e.g.
	 * 0.2 would mean that the sum of item-weights in p (= FolkRank preference vector) that
	 * refer to tags in the profile is 0.2 while the weight of the query tag is 0.8 
	 * @param weightedProfileTags the profile that should be modeled within the preference vector p
	 * @param tagIdColumMapping mapping from Tag.id to the index within the preference vector p
	 */
	@SuppressWarnings("unchecked")
	public static void setPreferenceVektorForWeightedTags(DoubleMatrix1D p, Tag query, double influenceOfPersonalization, List<WeightedTag> weightedProfileTags, Map<Integer, Integer> tagIdColumMapping) throws SQLException{
		double overallWeight = 0.0;
		for (WeightedTag wt: weightedProfileTags) {
			if(tagIdColumMapping.containsKey(new Integer(wt.tag.getId()))){
				overallWeight += wt.weight;
			}
		}
		for (WeightedTag wt: weightedProfileTags) {
			if(tagIdColumMapping.containsKey(new Integer(wt.tag.getId()))){
				p.set(tagIdColumMapping.get(new Integer(wt.tag.getId())), (wt.weight / overallWeight) * influenceOfPersonalization);
			}
		}
		
		//influence of query:
		p.set(tagIdColumMapping.get(new Integer(query.getId())), 1.0 - influenceOfPersonalization);
	}
	
	/**
	 * Sets the preference vector for certain tags (given as keywords) while the
	 * matrix only contains artificial tags a la "TxGy"
	 * @param p the preference vector that will be modified.
	 */
	public static void setPreferenceVektorForTxGyTags(DoubleMatrix1D p, List<Tag> tags, Map<String, Integer> artificialTagIdColumMapping) throws SQLException{
		String[] tagArray = new String[tags.size()];
		for (int i = 0; i < tagArray.length; i++) {
			tagArray[i] = tags.get(i).getKeyword();
		}
		setPreferenceVektorForTxGyTags(p, tagArray, artificialTagIdColumMapping);
	}
	
	/**
	 * Sets the preference vector for certain tags (given as keywords) while the
	 * matrix only contains artificial tags a la "TxGy"
	 * @param p the preference vector that will be modified.
	 */
	public static void setPreferenceVektorForTxGyTags(DoubleMatrix1D p, String[] tags, Map<String, Integer> artificialTagIdColumMapping) throws SQLException{
		Statement sqlStatement = null;
		ResultSet result = null;
		Statement sqlStatementB = null;
		ResultSet resultB = null;
		sqlStatement = ObjectFactory.ds.getConnection().createStatement();
		sqlStatementB = ObjectFactory.ds.getConnection().createStatement();
		List<String> artTagIds = new ArrayList<String>();
		int tagId = 0;
		for (int i = 0; i < tags.length; i++) {
			result = sqlStatement.executeQuery("SELECT id FROM Tag where keyword = \"" + tags[i] + "\"");
			if(result.next()){
				tagId = result.getInt(1);
				resultB = sqlStatementB.executeQuery("SELECT distinct groupId FROM userTagGroupResource where tagId = '" + tagId + "'");
				while(resultB.next()){
					artTagIds.add(FolkRankWithTxGyTags.constructTxGyTag(tagId, resultB.getInt(1)));
				}
			}
		}
		double weight = 1.0 / new Integer(artTagIds.size()).doubleValue();
		for (String tag : artTagIds) {
			p.set(artificialTagIdColumMapping.get(tag), weight);
		}
	}
}
